
require 'appwiki/note/tag.rb'
require 'appwiki/user/user.rb'
require 'find'

# require_gem 'acts_as_taggable'

#
# members of a given subject; as a way to do group forming
# 
class NotesUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :note
  def update
    super
  end
end

#
# recent observers of a given subject; used for sense of smell peer signalling
# 
class NotesVisitor < ActiveRecord::Base
  belongs_to :user
  belongs_to :note
  def update
    super
  end
end

#
# note to note relationships; used for building persistent webs of relationships
# 
class NotesRelation < ActiveRecord::Base
  belongs_to :note
  def update
    super
  end
end

#
# 'note' defines a typical web 2.0 kind of database object with title,link,description etc.
# also defines ideas like having relationships between notes and persons and the like
# notes are organized as a tree; you can ask for them with a file path uid
#
class Note < ActiveRecord::Base

    def update
      super
    end

    # acts_as_taggable

    belongs_to  :user
    has_many    :users,              :through => :notes_users

    has_many    :notes_users,        :dependent => :nullify
    has_many    :notes_visitors,     :dependent => :nullify
    has_many    :notes_relations,    :dependent => :nullify

    validates_length_of :uid,
                        :within => 1..20

    validates_format_of :uid,
                        :with => /^([a-z0-9_]+){0,2}[a-z0-9_]+$/i,
                        :on => :create,
                        :message => "can only contain letters and digits"

    serialize :extended

    # this just confuses things so leave it off
    # acts_as_tree :order => object_id

    # actually we cannot validate here because link is optional
    # validates_format_of :link, :with => /^(ftp|https?):\/\/((?:[-a-z0-9]+\.)+[a-z]{2,})/

    ##########################################################################################
    # treat extended variables as a hash; as a substitute for a slower separate table approach
    ##########################################################################################

    def []=(a,b)
      if a && self.has_attribute?(a)
        super(a,b)
      else
        self.extended = {} if !self.extended || !self.extended.is_a?(Hash)
        self.extended[a.to_s] = b
      end
    end

    def [](a) 
      if a
        return super(a) if self.has_attribute?(a)
        return self.extended[a.to_s] rescue nil
      end
      return nil
    end

    ####################################################################
    # recent visitors
    ####################################################################

    def activity_remember(user_id)
      user_id = user_id.to_i
      return if !user_id
      # first remove this user from having recently seen this note
      NotesVisitor.delete_all "user_id = #{user_id} and note_id = #{self.id}"
      # then append this user as having recently seen this note
      begin
        n = NotesVisitor.new(:user_id => user_id, :note_id => self.id, :modified_at => Time.now )
        n.save
      rescue
      end
      # then truncate the set of notes; however must truncate from the head
      begin
        count = 64
        NotesVisitor.find(:all,:conditions[":note_id = ?", self.id],:order=>"id DESC").each do |x|
          count = count - 1
          if count <= 0
             NotesVisitor.find_by_sql("DELETE FROM notes_visitor WHERE id = #{x.id}")
          end
        end
        # this would be more elegant
        # NotesVisitor.find_by_sql("DELETE FROM notes_visitors WHERE id in ( select id from notes_visitors where note_id = #{self.id} offset 64 );")
      rescue
      end
    end

    def activity_get(offset=0,limit=64)
      # return a collection of recent visitors based on the recent visitors table
      results = []
      NotesVisitor.find(:all,:conditions=>["note_id = ?",self.id],:order=>"id DESC").each do |x|
        results << User.find_by_sql("select login,title,link,description,depiction from users where id = #{x.user_id}").first;
      end
      return results
      # this would be more elegant
      # results = User.find_by_sql("SELECT * from USERS WHERE id in ( select user_id from notes_visitors where note_id = #{self.id} );");
      #"select users.id from users,notes_visitors where users.id="
    end

    ####################################################################
    # deal with users who are members/observers of a specific note
    ####################################################################

    # returns true if a given user is a member of this note - does include the sponsor!
    def is_a_member?(user_id)
      return true if user_id && self.sponsor == user_id
      return user_id && NotesUser.find(:first, :conditions => ["user_id = ? and note_id = ?",user_id,self.id ] )
    end

    # allows a member to join this note - a sponsor cannot join!
    def join(user_id,group=nil,role=nil)
      if !is_a_member?(user_id)
        begin
          x = NotesUser.new(:user_id => user_id, :note_id => self.id )
          x.group = group.id if group
          x.role = role if role
          x.save
        rescue
          return false
        end
      end
      return true
    end

    # stop being a member of this note; a sponsor cannot leave.
    def leave(user_id)
      if is_a_member?(user_id)
        begin
          NotesUser.find_by_sql("DELETE FROM notes_users WHERE note_id = #{self.id} and user_id = #{user_id}")
        rescue
          return false
        end
      end
      return true
    end

    # return a set of notes that a user is a member of. does not including notes they sponsored!
    def self.get_memberships(user_id)
      memberships = Array.new
      results = NotesUser.find(:all, :conditions => ["user_id = ?",@user_id], :order => "id DESC", :offset => 0, :limit => 10)
      results.each do |result|
        note = self.find_by_id(result.group_id)
        @memberships << note if note
      end
      return memberships
    end

    # returns a set of user objects that are members of this note. does not include the sponsor!
    def get_members()
      res = Array.new
      results = NotesUser.find(:all, :conditions => ["note_id = ?",self.id], :order => "id DESC", :offset => 0, :limit => 10)
      results.each do |result|
        user = User.find_by_id(result.user_id)
        res << user if user
      end
      return res
    end

    # find primary sponsor associated with this note; this is distinct from membership
    def get_sponsor()
      user = User.find_by_id(self.sponsor)
      if !user
        user = User.new
        user.login = "anonymous"
      end
      return user
    end

    # find name of sponsor of this note
    def get_sponsor_login
      @user = User.find_by_id self.sponsor
      return @user.login if @user
      return nil
    end

    ####################################################################
    # permissions
    ####################################################################

    # set privacy levels associated with a note; this is done elsewhere currently
    def set_privacy(user=nil,level=0)
      # currently 0 = public, 1 = public but read only, 2 = private, 3 = private and read only
      # in all cases if you are a member you can see it
    end

    def may_view(user)
      return self.perms == nil || self.perms == 0 || self.perms == 1 || ( user && self.is_a_member?(user.id) )
    end

    def may_edit(user)
      return self.perms == nil || self.perms == 0 || self.perms == 1 || ( user && self.is_a_member?(user.id) )
    end

    ####################################################################
    # filesystem type operations
    ####################################################################

    # find roots of the filesystem
    def self.find_roots(user=nil,offset=0,limit=100)
      candidates = Note.find(:all,:conditions => ['parent = ?',0],:order=>"id desc",:offset=>offset,:limit=>limit)
      results = []
      candidates.each do |x|
        if x.may_view(user)
          results << x
        end
      end
      return results
    end

    # find children of a folder; this is not incredibly well written TODO
    def find_children(offset=0,limit=100,newerthan=0,handler=nil)
      if !handler
        if !newerthan
          return Note.find(:all,:conditions => ['parent = ?',self.id],:order=>"id desc",:offset=>offset,:limit=>limit)
        else
          return Note.find(:all,:conditions => ['id > ? and parent = ?',newerthan,self.id],:order=>"id desc",:offset=>offset,:limit=>limit)
        end
      else
        if !newerthan
          return Note.find(:all,:conditions => ['parent = ? and handler = ?',self.id,handler],:order=>"id desc",:offset=>offset,:limit=>limit)
        else
          return Note.find(:all,:conditions => ['id > ? and parent = ? and handler = ?',newerthan,self.id,handler],:order=>"id desc",:offset=>offset,:limit=>limit)
        end
      end
    end

    # find by a path such as /site/anselm/files/faves
    def self.find_by_tokens(user,tokens)
      return nil if !tokens
      p = 0
      n = nil
      tokens.each do |u|
        if u && u.length > 0 
          n = Note.find(:first,:conditions=>['uid=? and parent=?',u,p])
          return nil if !n
          p = n.id
        end
      end
      return n
    end

    # find by a path such as /site/anselm/files/faves; returning the best leaf and the whole path too
    def self.find_best_by_tokens(user,tokens)
      p = 0
      best = nil
      found = []
      tokens.each do |u|
        if u && u.length > 0 
          n = Note.find(:first,:conditions=>['uid=? and parent=?',u,p])
          return best,found if !n
          best = n
          p = n.id
          found << u
        end
      end
      return best,found
    end

    # find by a path where the path is supplied as a single string with '/' as separators
    def self.find_by_path(user,path)
      return nil if !path
      return self.find_by_tokens(user,path.split('/'))
    end

    # return fully qualified url mapping for this note
    # TODO shame we have to know the host name here.
    def get_url
      path = nil
      note = self
      while true
        path = "/#{note.uid}#{path}"
        return "http://#{@@sethostname}#{path}" if !note.parent
        note = Note.find_by_id(note.parent)
        return "http://#{@@sethostname}#{path}" if !note
      end
      return nil
    end

    # return an url mapping for this note
    def relative_url
      path = nil
      note = self
      while true
        path = "/#{note.uid}#{path}"
        return path if !note.parent
        note = Note.find_by_id(note.parent)
        return path if !note
      end
      return nil
    end

    def get_file_path
      return relative_url
    end

    # return tokens
    def relative_tokens
      path = []
      note = self
      while true
        path << note.uid
        return path.reverse if !note.parent
        note = Note.find_by_id(note.parent)
        return path.reverse if !note
      end
      return nil
    end

    # get parent as a node
    def get_parent
      note = self
      return Note.find_by_id(note.parent)
    end

    # return just the parent as an url path
    def relative_url_parent
      note = self
      return note.get_parent().relative_url
    end

    ################################################################
    # set note properties; must not save anything yet
    ################################################################

    def set(params,user)

      # delete crap
      # TODO need to have a schema to define what a note can hold more clearly.
      params.delete(:members)
      params.delete('members')
      params.delete(:depiction)
      params.delete('depiction')
      params.delete(:submit)
      params.delete('submit')
      params.delete(:json)
      params.delete('json')
      params.delete(:sponsor)
      params.delete('sponsor')
      params.delete(:referer)
      params.delete('referer')
      params.delete(:id)
      params.delete('id')
      params.delete('created_at')
      params.delete(:created_at)

      # for now only sponsor can edit a thing
      # TODO later anybody who is a member can do so too
      if self.sponsor && self.sponsor != params[:sponsor]
        self.errors.add("sponsor","only the original sponsor can modify this")
      end

      # set extended parameters
      params.each do |x,y|
        next if self.has_attribute?(x)
        x = x.to_s
        y = y.to_s
        if x=="depiction" || x=="submit" || x=="referer" || x=="_" || x=="id" || x=="x" || x=="y" || x=="upload" || x=="path" 
          # ignore these for now
          # xxx; we need to associate a schema with each note.
        else
          next if !x || x.to_s.length < 1
          y = y.strip if y && y.class == "String"
          next if !y || y.to_s == '' || (y.class=="String" && y.length < 1)
          self[x] = URI.escape(y)
        end
      end

      # do not change perms if not sponsor
      if self.sponsor && self.sponsor != params[:sponsor]
        params.delete(:perms)
        params.delete('perms')
      end

      # do not change sponsor ever if set (from here anyway)
      if self.sponsor && self.sponsor > 0
      else
        self.sponsor = user.id if user
        self.sponsor = 0 if !user
      end

      # set parameters
      # TODO should really use a schema to guide what can be legally set
      params.each { |x,y|
        if x && y && y.to_s.length > 0 && self.has_attribute?(x)
          self.send(x.to_s+'=',y.to_s)
        end
      }

      # validate
      self.valid?

      # debug
      #puts "note::create() have a note #{self.id} #{self.uid} #{params[:uid]} #{params['uid']}" if @@debug
      #self.errors.each { |x,y| puts "note::create error #{x} #{y}" } if @@debug

    end

    #
    # save override; auto-tag and set parent to 0 instead of undefined
    #
    def save
      # make parent 0 if nil because sql differentiates between 0 and nil
      self.parent = 0 if !self.parent
      # finalize it
      state = super
      # if succeeded then apply tags after it has a valid id - it should have succeeded
      if state
        if self.id && self.tagstring
          Note.find_by_sql("DELETE FROM notes_tags WHERE note_id = #{self.id}")
          self.tag self.tagstring
        end
      end
      return state
    end

    #
    # get a child by name
    #
    def Note.get_child(uid,parent = nil)
      parent_id = 0
      parent_id = parent.id if parent
      note = Note.find(:first,:conditions=>["parent = ? and uid = ?",parent_id,uid])
      return note
    end

    #
    # guarantee that a path such as /anselm/files/faves exists
    # TODO disallow creation of children if parent is not publically editable xxx
    #
    def Note.make_path(user,tokens,params={})
      parent = nil
      path = []
      tokens.each do |token|
        note = Note.get_child(token,parent)
        if !note
          #return nil if parent && parent.sponsor && (user = nil || parent.sponsor != user.id)
          params[:uid] = token
          params[:handler] = 'folder' if !params[:handler]
          params[:sponsor] = user.id if user
          params[:sponsor] = parent.sponsor if parent
          params[:parent] = parent.id if parent
          note = Note.new
          note.set(params,user)
          state = note.save
          return nil if !state
        end
        parent = note
        path << note
      end
      return path
    end

    #
    # insert a child below the focus granting a new name if need be
    # TODO should later disallow creation of children if parent is not publically editable xxx
    #
    def make_child(user,params,name=nil)
      note = nil
      if name && name != "#"
        note = Note.get_child(name, self)
      end
      note = Note.new if !note
      params[:sponsor] = user.id if user
      params[:parent] = self.id
      params[:uid] = "invalid"
      note.set(params,user)
      state = note.save
      if state
        if !name || name == "#"
          note.uid = note.id
        else
          note.uid = name
        end
        puts "note::make_child() saved! as #{note.uid}" if @@debug
        note.save
        return note
      end
      puts "note::make_child() failed! for #{params}" if @@debug
      return nil
    end

    ##################################################################
    # destroy
    ##################################################################

    def destroy
      # destroy all relationships TODO
      # destroy all children
      # destroy files on disk
      super
    end

    ##################################################################
    # json output
    # TODO replace with library version
    ##################################################################

    def write_json_item
      # use to_json
      # "({"+note.attribute_names.collect{|x| x.to_s + ":'#{escape(note[x])}'" }.join(",") + "})"

      note = self
      buf = nil
      note.attribute_names.each do |x|
        # apparently 'parent' is reserved in javascript or in prototype or something xxx
        if x != 'extended' && x != 'parent' && note[x]
          if note[x].kind_of?(Integer) || note[x].kind_of?(Float)
            buf = buf+",\n" if buf
            buf = "#{buf} #{x}:#{note[x]}"
          elsif note[x].kind_of? Time
            buf = buf+",\n" if buf
            buf = "#{buf} #{x}:'#{note[x]}'"
          elsif note[x].kind_of?(String) && note[x].length
            buf = buf+",\n" if buf
            # escape these again - note they may already be escaped...
            buf = "#{buf} #{x}:'#{URI.escape(note[x]).gsub(/\'/,'%27')}'"
          end
        end
      end
      # using hashes (see legacy block below for context)
      if note.has_attribute?("extended") && note.extended && note.extended.is_a?(Hash)
        note.extended.each do |x,y|
          buf = buf+",\n" if buf
          # these are already stored escaped (due to quirk in note.rb); leave escaped
          # xxx using extended is just ugly! improve test for integer; need rdf schemas
          # xxx this whole thing should be replaced by an rdf like back end with formal attributes
          if y.to_i > 0 && "#{y.to_i}".length == y.strip.length && "#{y.to_i}" == y.strip
            buf = "#{buf} #{x}:#{y}"
          else
            buf = "#{buf} #{x}:'#{y}'"
          end
        end
      end
      # legacy block of code here - can remove or flush db whenever i wish xxx
      if note.has_attribute?("extended") && note.extended && note.extended.is_a?(String)
        note.extended.split('&').each do |v|
          x,y = v.split('=')
          buf = buf+",\n" if buf
          # these are already stored escaped (due to quirk in note.rb); leave escaped
          # TODO pretty ugly! improve test for integer; need rdf schemas
          if y.to_i > 0
            buf = "#{buf} #{x}:#{y}"
          else
            buf = "#{buf} #{x}:'#{y}'"
          end
        end
      end
      # wrap up a bit
      return "{\n"+buf+"\n}" if buf
      return ""
    end

    ##################################################################
    # file system scan
    ##################################################################

  # scan part of filesystem for out of band content
  # how do we deal with deleted or moved files? should we indicate lineage?
  def rescan
    folder = "./"
    # probably do want access control lists loaded off disk too TODO
    ignore = [".svn",".htaccess"]
    accept = ["appwiki","hook"]  # TODO use the boot namespace not hardcoded one
    Find.find(folder) do |filepath|
      fdir,fname = File.split(filepath)
      if ignore.eql?(fname.tolower) || fname.length < 1 || !accept.include?(fdir)
        Find.prune
      else
        handler = 'file'
        if FileTest.directory? filepath
          handler = 'folder'
        else
          handler = fname.split('.').last if handler.index('.') != nil
          # handler = fbase.scan(/\.(*)$/
          # TODO; lookup a best mimetype or handler??? in some kind of table???
        end
        parent = Note.find_by_tokens(nil,filepath.downcase.split('/')[0..-2])
        note = Note.find_by_path(filepath.downcase)
        puts "accepting #{filepath}"
        if note
          # TODO update?
        elsif parent
          note = Note.new
          note.uid = fname
          note.parent = parent
          note.handler = handler
          # description = File.read(filename) if fbase =~ /\.txt$/
        end
      end
    end
  end

  ##################################################################
  # schema management
  ##################################################################

  def self.schema
    self.schema_001
  end

  def self.schema_drop
    return if !table_exists?
    ActiveRecord::Schema.define do
      begin
        drop_table :notes_users
      rescue
      end
      begin
        drop_table :notes_tags
      rescue
      end
      begin
        drop_table :notes
      rescue
      end
      begin
        drop_table :notes_visitors
      rescue
      end
    end
  end

  def self.schema_001
    ActiveRecord::Schema.define do
      begin
        # relationships between two notes
        # the same relationship can exist more than once; it is invariant on user
        create_table :notes_relations do |t|
          t.column :user_id,         :integer, :null => false
          t.column :note_id,         :integer, :null => false
          t.column :target_id,       :integer, :null => false
          t.column :role,            :text # the kind of relationship... open ended.
        end
      rescue
      end
      begin
        # a table of recent visitors
        create_table :notes_visitors do |t|
          t.column :note_id,         :integer, :null => false
          t.column :user_id,         :integer, :null => false
          t.column :modified_at,     :datetime
        end
      rescue
      end
      begin
        # a table of who { owns, cited, owns, observes } or otherwise has an interest in a note.
        # since it is unclear to me how group ownership would work, i permit it as a distinct idea
        # a note itself also has a sponsor... it overlaps the idea of ownership here as well.
        create_table :notes_users do |t|
          t.column :user_id,         :integer, :null => false
          t.column :group_id,        :integer, :null => false
          t.column :note_id,         :integer, :null => false
          t.column :role,            :text # the kind of relationship... open ended.
        end
      rescue
      end
      begin
        # a table of tags associated with a note
        create_table :notes_tags do |t|
          t.column :tag_id,          :integer, :null => false
          t.column :note_id,         :integer, :null => false
        end
        remove_column :notes_tags, :id
      rescue
      end
      begin
        # a table of extended fields for notes { not used }
        create_table :notes_extended do |t|
          t.column :note_id,         :integer, :null => false
          t.column :kind,            :string
          t.column :name,            :string
          t.column :value,           :text
        end
      rescue
      end
      begin
        # a table of 'notes' ; a generic 'store stuff' concept
        create_table :notes do |t|
          t.column :id,              :serial, :null => false
          t.column :uid,             :text    # globally unique uid
          t.column :parent,          :integer # directed acyclic graph
          t.column :root,            :integer # second root of the graph for any leaf
          t.column :handler,         :text    # our own type concept; mime handler
  
          t.column :title,           :text    # similar to rss, basically a catch all for common stuff
          t.column :link,            :text
          t.column :description,     :text
          t.column :tagstring,       :text
          t.column :depiction,       :text
          t.column :extended,        :text    # bucket for arbitrary fields ( a ruby hash )
  
          t.column :sponsor,         :integer # also see separate table for these; this is slightly redundant for convenience
          t.column :perms,           :integer # a concept of read/write/edit/delete permissions
          t.column :nchildren,       :integer # a roll up computation done periodically
          t.column :nobservers,      :integer # roll up computation done periodically
          t.column :location,        :text    # location in english
          t.column :depiction,       :text    # depiction
          t.column :latmin,          :float
          t.column :lonmin,          :float
          t.column :latmax,          :float   # optional extent based boundary for notes
          t.column :lonmax,          :float
          t.column :begins,          :datetime # timewise start
          t.column :ends,            :datetime # timewise end
          t.column :created_at,      :datetime
          t.column :modified_at,     :datetime
        end
      rescue
      end
    end
  end
end

#
# Make schema right now! if needed
#
Note.schema
